feat(types)!: mirror eight ObjectViewSchema keys (three by spec reference) and retire viewTabBar (objectui#7779) - #7922
Conversation
…ence) and retire viewTabBar (objectui#7779) `ObjectViewSchema`'s TypeScript face declared ten keys its Zod mirror never did (objectui#7279's `UnmirroredDeclared` reading), so a document authoring any of them passed the validator unexamined through `BaseSchema`'s passthrough. Maintainer ruling B (2026-09-06): liveness first, then mirror-or-retire per key. - objectql.zod.ts: `navigation` / `searchableFields` / `filterableFields` are `SpecListViewSchema.shape.*` by reference (identity-pinned); `allowCreateView` / `viewActions` are the sibling `ViewSwitcherSchema` slots by reference (the renderer forwards both verbatim into the `view-switcher` node); `defaultViewType` (the declaration's seven-value union) / `defaultListView` / `showViewSwitcher` are local literals after a reader census on `plugin-view/src/ObjectView.tsx`; `viewTabBar` is a `retirementTombstone()` (zero reads — the tab-bar config is `ViewTabBar`'s `config` prop from the host, never a node key) - objectql.ts: `viewTabBar?: never` with the RETIRED docblock (the #7322 shape) - `listViews` stays unmirrored on the ruling's own fallback clause, with the measurement pinned against the spec: the declaration's `NamedListView` (about 52 members, seven read) and the spec's strict `ObjectListViewSchema` (requires `columns`, refuses `options`, tuple filters and `default`) are incompatible, and the spec value refuses the named views the docs teach - zod-mirror-parity: the entry shrinks to `listViews`; `ObjectViewSchema` joins `SPEC_DERIVED_PAIRS` by a real code reference; header figures 14 / 96 -> 14 / 87, split 1 / 2 + 13 / 94 -> 2 / 3 + 12 / 84, with the history sentences - object-view-spec-parity: `TS_ONLY_BACKLOG` shrinks to `listViews` + `onNavigate`; the audit figures move 11 / 13 -> 20 / 22 with the arithmetic - new pin object-view-unmirrored-keys-7779: reads derived off disk with controls, membership, spec/sibling identity, accept-and-survive plus wrong-typed refusal at the key per mirrored key, the tombstone on both faces, the `listViews` measurement, neighbour and passthrough controls, the two doc tables - docs: the plugin-view README and plugin-view.mdx import tables no longer teach `viewTabBar` as a node key - changeset: `@object-ui/types` minor, "Breaking for authored metadata" Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BAZFhALsQsGqxui8sNqM8s
…ect-view-unmirrored-keys
✅ Console Performance Budget
The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it. 📦 Bundle Size Report
Size Limits
|
Contract review (clause ②) — PASS — PR #7922 at head
|
| # | claim | reading | verdict |
|---|---|---|---|
| 1 | Ruling B executed nine-for-ten: three spec-modelled keys mirrored by reference to SpecListViewSchema.shape.*; two by reference to the sibling ViewSwitcherSchema slots the renderer forwards into; three local literals after a reader census; viewTabBar retired |
Diff read: navigation: SpecListViewSchema.shape.navigation, searchableFields / filterableFields likewise; allowCreateView / viewActions: ViewSwitcherSchema.shape.*; defaultViewType seven-value enum (host-composition-only chart/tree excluded per objectui#5321), defaultListView string, showViewSwitcher boolean; viewTabBar: retirementTombstone(...) + ?: never. No local restatement of a spec shape — the drift objectui#4588 keeps paying for is avoided by construction. |
correct |
| 2 | listViews stays in the ledger on the ruling's own fallback clause, with the measurement pinned against the spec (the strict ObjectListViewSchema refuses the named views this package's own docs teach; the local NamedListView would enforce ~45 unread members) |
Exactly the ruling's stop condition; ⛔ not z.any(). The value-type decision is the maintainer's box, not this review's. |
correct |
| 3 | Reader census on the object-view node renderer with schema.objectName / schema.layout as firing controls; viewTabBar zero reads repo-wide outside @object-ui/types; ViewTabBarConfig reaches the bar only as a host-composed prop (ADR-0053) |
Derived off disk in the pin. | correct |
| 4 | Ledger moves: UnmirroredDeclared 14/96 → 14/87; ObjectViewSchema re-derived into SPEC_DERIVED_PAIRS by a real code reference; object-view-spec-parity's shrink-only backlog pin moved by a gate |
Consistent with the parity-ledger discipline (PR #7776 / #7832 shape). | correct |
| 5 | 52-test pin; ablation 37 red / 100 green across three files with the compile-time ledger naming the entry | Discriminating direction. | correct |
| 6 | Two doc tables corrected (plugin-view README, plugin-view.mdx), zero fenced snippets moved |
content/docs/releases/ untouched. |
accepted |
② semver
@object-ui/types minor, "Breaking for authored metadata" opening — this repo's fixed-group convention (breaking ships as minor; check-changeset-no-major enforced). Correct.
③ Boundary flags
- Sibling [finding]
ComponentInput.defaultValue?: any(packages/types/src/base.ts:550) is why the marked plugin example inskills/objectuimust carry a bareany— tighten the contract tounknownand the baseline row retires with the guide #7493 item ① (PR feat(types)!: retire ComponentInput.label / defaultValue / advanced as ADR-0049 tombstones (objectui#7493 item 1 / objectui#7781) #7932) shareszod-mirror-parity.test.tsat a different region; the union is both history sentences plus re-derived totals, which the objectui#7279 pin checks. Merge queue arbitrates; no hand-ordering. NamedListView's ~45 unread members are a liveness question of their own (noted by the dev, not this card's).
Evidence and landing
Checks on 94e7ac47: 29 success / 3 skipped / 0 red; mergeable_state: clean; governed-surface predicate 0 of 8 paths. Clearing, same stroke: needs:contract-review off PR #7922 (the card carried none) with provenance; landing from this seat: ready + auto-merge SQUASH.
Generated by Claude Code
…tion lines (objectui#7779 pin) `packages/types/src/__tests__/object-view-unmirrored-keys-7779.test.ts` landed on `main` (PR #7922) one minute after this branch merged its base, so its `REGISTRATION_TEXT` hard-codes three `plugin-view/src/index.tsx` registration lines that still carry the `ComponentInput.label` key this PR retires. The pin asserted a fact this change is the reason is no longer true. Each expected string is copied off the registration source rather than composed by hand: only the `label: '…', ` fragment is dropped, every other character is the line as `packages/plugin-view/src/index.tsx` now reads. The assertion stays an exact `toContain` over three entries — the pin's intent (the three local literals are still registered as editable inputs) survives the retirement. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BAZFhALsQsGqxui8sNqM8s
Fixes #7779
What this PR does
ObjectViewSchema's TypeScript face declared ten keys its Zod mirror never did (theUnmirroredDeclaredentry objectui#7279 re-derived), so a document authoring any of them passed the validator unexamined throughBaseSchema's passthrough while the published type invited the author to write it. Under the maintainer's ruling B on the card (2026-09-06, verbatim 「第二批(4 项,契约形状族) 帮我处理,全部同意」; record 5556357004) — liveness first, then mirror-or-retire per key — nine of the ten are dispositioned here and the tenth (listViews) stays in the ledger on the ruling's own fallback clause, with its measurement.Read on
origin/main6a9ee323(the branch base). The seat's premise atfa7d66c4re-verified on it:objectql.zod.ts:196, the ten declarations atobjectql.ts:1804–:1922, ledger rows:702/:864/:1437/:1604/:1855/:2866–:2867, all as stated; every reading below is bound to theObjectViewSchemablock, never to a name match (navigationandsearchableFieldsalso live on other interfaces in the same file).Per key
object-viewnode renderer —packages/plugin-view/src/ObjectView.tsx, registered byplugin-view/src/index.tsxnavigationSpecListViewSchema.shape.navigation:773schema.navigation;:1817searchableFieldsSpecListViewSchema.shape.searchableFields:1820filterableFieldsSpecListViewSchema.shape.filterableFields:1821allowCreateViewViewSwitcherSchema.shape.allowCreateView(views.zod.ts):1134— forwarded verbatim into theview-switchernode the renderer composes;ViewSwitcher.tsx:276reads itviewActionsViewSwitcherSchema.shape.viewActions:1135;ViewSwitcher.tsx:254–:256defaultViewType:769schema.defaultViewType OR 'grid'defaultListView:701–:702namedListViews?.[schema.defaultListView]showViewSwitcher:1910schema.showViewSwitcher === trueviewTabBar?: neveron the TS face,retirementTombstone()on the mirror@object-ui/types(two doc tables listed it as authorable, corrected here).ViewTabBarConfigreaches the tab bar only asViewTabBar'sconfigPROP, passed by app-shell (views/ObjectView.tsx:2825); plugin-view's ownObjectViewrenders no tab bar (ADR-0053). The 2026-07 audit had measured it "dead since introduction"listViews:698, plus sevenNamedListViewmembers — see belowPositive controls of the same
schema.KEYquery:objectNameandlayout(read);viewTabBarand the never-declaredviewSwitcherPosition(not read). The pin derives this set off disk, so each zero is a reading.Why
allowCreateView/viewActionsare sibling slots rather than restated literals: the renderer hands both to the switcher node unchanged, the sibling mirror already spells the identical shape (z.enum(['share', 'settings', 'duplicate', 'delete'])plus optionalicon), and one object for one key is what stops the two from drifting.defaultViewTypeis deliberately the declaration's seven values, not the spec's nine-value view-kind enum:chart/treeare host-composition-only on this node (objectui#5321) andNamedListView.typespells the same seven.Spec-side readings, through the installed pin
@objectstack/spec@17.2.0,uientry, measured from this worktree — 117 exported object schemas walked; control keysobjectName9 hits,columns15,navigation5,listViews1; each of the six local keys 0 hits:ListViewSchema.navigationandObjectListViewSchema.navigation: optional object (NavigationConfigSchema:modedefaulting topage,view,preventNavigation,openNewTab,size,width; strict, an unknown key is refused).{ view: 'summary_view' }parses withmodedefaulted; a bare string is refused.searchableFieldsandfilterableFieldson both view faces: optional array of string; the spec's own description offilterableFieldsis "Legacy shorthand for userFilters.fields — bare field names enabled for end-user filtering. Prefer userFilters".ViewSchema.listViews: optional record whose VALUE isObjectListViewSchemaby identity — strict (catchall never),columnsits only required key (a union),typea nine-value enum defaulting togrid.listViews— measured, and left in the ledger on the ruling's stop clauseWhat the two value types admit, and what the readers accept:
NamedListViewmembers off a named view —label,type,columns,filter,sort,options,data(derived off disk and pinned).NamedListViewdeclares about 52 members (objectql.ts:1957–:2134).ObjectListViewSchema) accepts the schema-catalog example ({ label, columns }) but REFUSES: the README / mdx form{ label: 'All Users' }(atcolumns); atypeplusfilterview with nocolumns(atcolumns);content/docs/api/schema-reference.md's ObjectQL tuple filter[["owner", "=", "..."]](atfilter.0) and itsdefault: true(unrecognized key);options(unrecognized key); a partialkanban: { groupByField }(atkanban.columns).schema-reference.mdauthors tuple filters; app-shell'sViewPreview.tsx:113spreads a spec-shaped view body intolistViews.So mirroring the spec value by reference loses documented behaviour, and mirroring the local
NamedListViewkey-for-key enforces about 45 members the renderer never reads — the "enforced dead key" ruling B refused for the six local keys. That is the ruling's stop condition for this key, and its own instruction is followed: the other spec-modelled keys are mirrored,listViewsstays in the ledger with the measurement (pinned against the SPEC schema, so a spec relaxation or a renderer change re-takes it instead of remembering it).z.any()is not used anywhere. The value-type decision goes to the maintainer in the report's open questions with the four-axis analysis.Ledger and pins
zod-mirror-parity.test.ts: theObjectViewSchemaentry shrinks 10 → 1 (listViews);SPEC_DERIVED_PAIRSgains the pair by a real code reference (the objectui#6705 scanner now seesSpecListViewSchema.shape.*in the initializer), so the split re-derives the entry into the SPEC-DERIVED half; header figures move 14 / 96 → 14 / 87 and 1 / 2 + 13 / 94 → 2 / 3 + 12 / 84, each with its history sentence (the PR test(types): re-derive ObjectViewSchema's side of the #6058 split as LOCAL and pin the header counts (objectui#7279) #7776 / PR feat(types)!: retire the legacy ActionSchema onSuccess/onFailure callback pair and delete ActionCallback (objectui#7068) #7832 shape). The objectui#7279 pin reads them off the header and agrees.object-view-spec-parity.test.ts— a seventh file, not on the dispatch's list, moved by a gate: itsTS_ONLY_BACKLOG"may shrink, never grow" pin held exactly these ten keys. It shrinks tolistViewsplusonNavigate; the audit figures move 11 / 13 → 20 / 22 with the arithmetic shown; the three spec-counterpart notes now read DONE.object-view-unmirrored-keys-7779.test.ts(52 tests): reads derived off disk with controls; membership on the mirror's own.shape; identity against the spec slots and the sibling slots; per mirrored key, accepted-and-survives plus a wrong-typed refusal AT the key (12 refusal probes, direct and through the union door); the tombstone on both faces (message, codeinvalid_type, describe equals message, TSneverat the type level plus@ts-expect-error); thelistViewsmeasurement against the spec; neighbour and passthrough controls; the two doc tables.packages/plugin-view/README.mdandcontent/docs/plugins/plugin-view.mdximport tables no longer teachviewTabBaras a node key — the only two pages that did; the diff moves zero fenced snippets.@object-ui/typesminor, "Breaking for authored metadata" opening, every key and its disposition named.Gates — exit captured before any pipe, verdict lines quoted, heavy runs under the shared verify lock
Taken on
e9f86194; the ratchet family re-run on the merged head94e7ac47(origin/mainf5d2acc3merged in; main moved nothing underpackages/typessince the base):pnpm --filter @object-ui/types build—VERDICT command-exit 0(dist completeness: 124 emitted files verified)pnpm exec vitest run --maxWorkers=2 packages/types/—Test Files 129 passed (129),Tests 2388 passed (2388),VERDICT command-exit 0; re-run at94e7ac47: the same figurespnpm --filter @object-ui/types type-check(tsc --noEmit && tsc -p tsconfig.examples.json && tsc -p tsconfig.test.json) —VERDICT command-exit 0;tsc -p tsconfig.test.json --listFileslists the new pin (1 hit); re-run at94e7ac47: green in the same verdictpaths, so@object-ui/typesresolves todist/*.d.ts):pnpm --workspace-concurrency=2 --filter @object-ui/plugin-list --filter @object-ui/plugin-view --filter @object-ui/app-shell type-checkafter building each dependency closure — all three echotype-check$ tsc --noEmit && tsc -p tsconfig.test.jsonandDone,VERDICT command-exit 0viewTabBar: { showAddButton: true }anddefaultViewType: 'tree'— exactly two TS2322 at those two lines (not assignable to type 'undefined';'"tree"' is not assignable to …'"kanban" | "grid" | …'), the eight-key literal beside them clean,--listFilesreadingpackages/types/dist/objectql.d.tswith 0srcreads; probe removed, tree cleanpnpm --filter @object-ui/types lint— 0 errors (274 pre-existingno-explicit-anywarnings, none in the touched regions)check-changeset-presence.mjs/-no-major/-fixed/-overwrite— all ✅;check-control-bytes.mjs✅ (6396 tracked text files);pnpm check:spec-symbols✅;check:doc-types✅;check:doc-fences✅;check:vi-mock-specifiers✅;check:vi-mock-inherit✅;check:shell-escape-residue✅;check:unreferenced-sources✅check:doc-snippetsexits 2PRECONDITION NOT MET(six unbuilt packages, none of them this PR's);check:readme-exportscould not judge oneplugin-gantttype (unbuilt dist) — theplugin-viewREADME was among the 43 READMEs it judged. Both are CI's runs; this PR's doc diffs move zero fenced snippets.scripts/pm/check-governed-merges.mjs --test, all 8 paths) —0 of 8 path(s) hit the register→ NOT governedobjectql.zod.tsalone reverted to the base blob, trap-guarded with absolute paths — mutation proven on disk (tombstone line and spec-reference line each 1 → 0; blob97b7a668vs HEAD44e31169); the three pins under it:Test Files 3 failed (3),Tests 37 failed | 100 passed (137)— 34 in the new pin, 2 inobject-view-spec-parity(backlog, audit figures), 1 in the ledger (SPEC_DERIVED_PAIRSre-derivation); the controls green;tsc -p tsconfig.test.jsonexit 2 with the compile-time ledger naming exactly'objectql.zod.ts#ObjectViewSchema'; restored viagit checkout HEAD -- path, blob44e31169equals HEAD's,git diff HEADempty, status cleanSibling overlap
#7493's dev touches
zod-mirror-parity.test.tsat thebase.zod.ts#ComponentInputSchemarows and its own header figures; this PR touches only theobjectql.zod.ts#ObjectViewSchemarows, theUnmirroredDeclaredfigures andSPEC_DERIVED_PAIRS. Textual overlap is possible on the header-figure lines; the union is both history sentences plus the re-derived totals, which the objectui#7279 pin checks. The merge queue rebuilds onmain;origin/mainis merged again when the sibling lands. Left DRAFT withneeds:contract-review; no ready flip, no enqueue.Noted for the seat, out of this card's scope: the
NamedListViewdeclaration itself carries about 45 members nothing reads off a named view — a liveness question of its own, not one of this card's ten keys.🤖 Generated with Claude Code
https://claude.ai/code/session_01BAZFhALsQsGqxui8sNqM8s
Generated by Claude Code